/*
Copyright SecureKey Technologies Inc. All Rights Reserved.

SPDX-License-Identifier: Apache-2.0
*/
package ichain_sdk

import (
	"math/rand"
	"time"

	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/common/logging"
	coptions "git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/common/options"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/core/config/lookup"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/core/cryptosuite"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/core/logging/api"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/ichain/context"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/ichain/endpoint"
	iam2 "git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/ichain/iam"
	sdkApi "git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/ichain_sdk/api"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/ichain_sdk/metrics"
	metricsCfg "git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/ichain_sdk/metrics/cfg"
	contextApi "git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/providers/context"
	core "git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/providers/core"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/providers/iam"
	"git.cloud.inspur.com/ichain/ichain-sdk-go/pkg/providers/ichain"
	"github.com/pkg/errors"
)

var logger = logging.NewLogger("ichainsdk")

// IChainSDK sdk对象
type IChainSDK struct {
	opts          options
	provider      contextApi.Providers
	cryptoSuite   core.CryptoSuite
	ProviderType  string
	clientMetrics *metrics.ClientMetrics
	sdkCacheParams
}

type configs struct {
	cryptoSuiteConfig core.CryptoSuiteConfig
	endpointConfig    ichain.EndpointConfig
	identityConfig    iam.IdentityConfig
	metricsConfig     metricsCfg.MetricsConfig
}

type options struct {
	Core              sdkApi.CoreProviderFactory
	IAM               sdkApi.IAMProviderFactory
	Service           sdkApi.ServiceProviderFactory
	Logger            api.LoggerProvider
	CryptoSuiteConfig core.CryptoSuiteConfig
	endpointConfig    ichain.EndpointConfig
	IdentityConfig    iam.IdentityConfig
	ConfigBackend     []core.ConfigBackend
	ProviderOpts      []coptions.Opt // Provider options are passed along to the various providers
	metricsConfig     metricsCfg.MetricsConfig
}

// Option configures the SDK.
type Option func(opts *options) error

type closeable interface {
	Close()
}

type contextCloseable interface {
	CloseContext(ctxt ichain.ClientContext)
}

func New(configProvider core.ConfigProvider, opts ...Option) (*IChainSDK, error) {
	return getSdkCache().get(configProvider, opts...)
}

func GetSDKCache() *sdkCache {
	return getSdkCache()
}

func newSDK(configProvider core.ConfigProvider, opts ...Option) (*IChainSDK, error) {
	pkgSuite := defPkgSuite{}
	return fromPkgSuite(configProvider, &pkgSuite, opts...)
}

func fromPkgSuite(configProvider core.ConfigProvider, pkgSuite pkgSuite, opts ...Option) (*IChainSDK, error) {
	coreProv, err := pkgSuite.Core()
	if err != nil {
		return nil, errors.WithMessage(err, "Unable to initialize core pkg")
	}

	iamProv, err := pkgSuite.IAM()
	if err != nil {
		return nil, errors.WithMessage(err, "Unable to initialize core pkg")
	}

	svc, err := pkgSuite.Service()
	if err != nil {
		return nil, errors.WithMessage(err, "Unable to initialize service pkg")
	}

	lg, err := pkgSuite.Logger()
	if err != nil {
		return nil, errors.WithMessage(err, "Unable to initialize logger pkg")
	}

	sdk := IChainSDK{
		opts: options{
			Core:    coreProv,
			IAM:     iamProv,
			Service: svc,
			Logger:  lg,
		},
	}

	err = initSDK(&sdk, configProvider, opts)
	if err != nil {
		return nil, err
	}

	return &sdk, err
}

func WithCryptoSuiteConfig(cryptoConfigs ...interface{}) Option {
	return func(opts *options) error {
		c, err := cryptosuite.BuildCryptoSuiteConfigFromOptions(cryptoConfigs...)
		if err != nil {
			return err
		}
		opts.CryptoSuiteConfig = c
		return nil
	}
}

func WithEndpointConfig(endpointConfigs ...interface{}) Option {
	return func(opts *options) error {
		c, err := endpoint.BuildConfigEndpointFromOptions(endpointConfigs...)
		if err != nil {
			return err
		}
		opts.endpointConfig = c
		return nil
	}
}

func WithIdentityConfig(identityConfigs ...interface{}) Option {
	return func(opts *options) error {
		c, err := iam2.BuildIdentityConfigFromOptions(identityConfigs...)
		if err != nil {
			return err
		}
		opts.IdentityConfig = c
		return nil
	}
}

func WithCorePkg(core sdkApi.CoreProviderFactory) Option {
	return func(opts *options) error {
		opts.Core = core
		return nil
	}
}

func WithIAMPkg(iam sdkApi.IAMProviderFactory) Option {
	return func(opts *options) error {
		opts.IAM = iam
		return nil
	}
}

func WithServicePkg(service sdkApi.ServiceProviderFactory) Option {
	return func(opts *options) error {
		opts.Service = service
		return nil
	}
}

func WithLoggerPkg(logger api.LoggerProvider) Option {
	return func(opts *options) error {
		opts.Logger = logger
		return nil
	}
}

func WithMetricsConfig(metricsConfigs ...interface{}) Option {
	return func(opts *options) error {
		c, err := metricsCfg.BuildConfigMetricsFromOptions(metricsConfigs...)
		if err != nil {
			return err
		}
		opts.metricsConfig = c
		return nil
	}
}

func WithProviderOpts(sopts ...coptions.Opt) Option {
	return func(opts *options) error {
		opts.ProviderOpts = append(opts.ProviderOpts, sopts...)
		return nil
	}
}

func WithErrorHandler(value ichain.ErrorHandler) Option {
	return WithProviderOpts(withErrorHandlerProviderOpt(value))
}

type providerInit interface {
	Initialize(providers contextApi.Providers) error
}

func initSDK(sdk *IChainSDK, configProvider core.ConfigProvider, opts []Option) error { //nolint
	for _, option := range opts {
		err := option(&sdk.opts)
		if err != nil {
			return errors.WithMessage(err, "Error in option passed to New")
		}
	}

	if sdk.opts.Logger == nil {
		return errors.New("Missing logger from pkg suite")
	}
	logging.Initialize(sdk.opts.Logger)

	cfg, err := sdk.loadConfigs(configProvider)
	if err != nil {
		return errors.WithMessage(err, "failed to initialize configuration")
	}

	rand.Seed(time.Now().UnixNano())

	userStore, err := sdk.opts.IAM.CreateUserStore(cfg.identityConfig)
	if err != nil {
		return errors.WithMessage(err, "failed to create state store")
	}

	signingManager, err := sdk.opts.Core.CreateSigningManager(sdk.cryptoSuite)
	if err != nil {
		return errors.WithMessage(err, "failed to create signing manager")
	}

	identityManagerProvider, err := sdk.opts.IAM.CreateIdentityManagerProvider(cfg.endpointConfig, sdk.cryptoSuite, userStore)
	if err != nil {
		return errors.WithMessage(err, "failed to create identity manager provider")
	}

	infraProvider, err := sdk.opts.Core.CreateIChainProvider(cfg.endpointConfig)
	if err != nil {
		return errors.WithMessage(err, "failed to create infra provider")
	}

	localDiscoveryProvider, err := sdk.opts.Service.CreateLocalDiscoveryProvider(cfg.endpointConfig)
	if err != nil {
		return errors.WithMessage(err, "failed to create local discovery provider")
	}

	chainProvider, err := sdk.opts.Service.CreateChainProvider(cfg.endpointConfig, sdk.opts.ProviderOpts...)
	if err != nil {
		return errors.WithMessage(err, "failed to create chain provider")
	}

	sdk.initMetrics(cfg)

	sdk.provider = context.NewProvider(context.WithCryptoSuiteConfig(cfg.cryptoSuiteConfig),
		context.WithEndpointConfig(cfg.endpointConfig),
		context.WithIdentityConfig(cfg.identityConfig),
		context.WithCryptoSuite(sdk.cryptoSuite),
		context.WithSigningManager(signingManager),
		context.WithUserStore(userStore),
		context.WithLocalDiscoveryProvider(localDiscoveryProvider),
		context.WithIdentityManagerProvider(identityManagerProvider),
		context.WithIChainProvider(infraProvider),
		context.WithChainProvider(chainProvider),
		context.WithClientMetrics(sdk.clientMetrics),
	)

	if pi, ok := infraProvider.(providerInit); ok {
		err = pi.Initialize(sdk.provider)
		if err != nil {
			return errors.WithMessage(err, "failed to initialize infra provider")
		}
	}

	if pi, ok := localDiscoveryProvider.(providerInit); ok {
		err = pi.Initialize(sdk.provider)
		if err != nil {
			return errors.WithMessage(err, "failed to initialize local discovery provider")
		}
	}

	if pi, ok := chainProvider.(providerInit); ok {
		err = pi.Initialize(sdk.provider)
		if err != nil {
			return errors.WithMessage(err, "failed to initialize chain provider")
		}
	}

	logger.Debug("SDK initialized successfully")
	return nil
}

func (sdk *IChainSDK) Close() {
	getSdkCache().releaseSDK(sdk)
}

func (sdk *IChainSDK) closeSDK() {
	logger.Infof("%s sdk is closed", sdk.key)
	if pvdr, ok := sdk.provider.LocalDiscoveryProvider().(closeable); ok {
		pvdr.Close()
	}
	if pvdr, ok := sdk.provider.ChainProvider().(closeable); ok {
		pvdr.Close()
	}
	sdk.provider.IChainProvider().Close()
}

func (sdk *IChainSDK) CloseContext(ctxt ichain.ClientContext) {
	logger.Debug("Closing clients for the given context...")
	if pvdr, ok := sdk.provider.LocalDiscoveryProvider().(contextCloseable); ok {
		logger.Debugf("Closing local discovery provider...")
		pvdr.CloseContext(ctxt)
	}
	if pvdr, ok := sdk.provider.ChainProvider().(contextCloseable); ok {
		logger.Debugf("Closing context in chain client...")
		pvdr.CloseContext(ctxt)
	}
}

func (sdk *IChainSDK) Config() (core.ConfigBackend, error) {
	if sdk.opts.ConfigBackend == nil {
		return nil, errors.New("unable to find config backend")
	}
	return lookup.New(sdk.opts.ConfigBackend...), nil
}

func (sdk *IChainSDK) Context(options ...ContextOption) contextApi.ClientProvider {

	clientProvider := func() (contextApi.Client, error) {
		identity, err := sdk.newIdentity(options...)
		if err == ErrAnonymousIdentity {
			identity = nil
			err = nil
		}
		return &context.Client{Providers: sdk.provider, SigningIdentity: identity}, err
	}

	return clientProvider
}

func (sdk *IChainSDK) ChainContext(chainID string, options ...ContextOption) contextApi.ChainProvider {

	chainProvider := func() (contextApi.Chain, error) {

		clientCtxProvider := sdk.Context(options...)
		return context.NewChain(clientCtxProvider, chainID)

	}

	return chainProvider
}

func (sdk *IChainSDK) initializeCryptoSuite(cryptoSuiteConfig core.CryptoSuiteConfig) error {
	var err error
	sdk.cryptoSuite, err = sdk.opts.Core.CreateCryptoSuite(cryptoSuiteConfig)
	if err != nil {
		return errors.WithMessage(err, "failed to initialize crypto suite")
	}
	sdk.ProviderType = cryptoSuiteConfig.SecurityProvider()
	return nil
}

func (sdk *IChainSDK) loadConfigs(configProvider core.ConfigProvider) (*configs, error) {
	c := &configs{
		identityConfig:    sdk.opts.IdentityConfig,
		endpointConfig:    sdk.opts.endpointConfig,
		cryptoSuiteConfig: sdk.opts.CryptoSuiteConfig,
		metricsConfig:     sdk.opts.metricsConfig,
	}

	var configBackend []core.ConfigBackend
	var err error

	if configProvider.Func != nil {
		configBackend, err = configProvider.Func()
		if err != nil {
			return nil, errors.WithMessage(err, "unable to load config backend")
		}
	}

	c.cryptoSuiteConfig, err = sdk.loadCryptoConfig(configBackend...)
	if err != nil {
		return nil, errors.WithMessage(err, "unable to load crypto suite config")
	}

	err = sdk.initializeCryptoSuite(c.cryptoSuiteConfig)
	if err != nil {
		return nil, errors.WithMessage(err, "unable to initialize cryptosuite using crypto suite config")
	}

	c.endpointConfig, err = sdk.loadEndpointConfig(configBackend...)
	if err != nil {
		return nil, errors.WithMessage(err, "unable to load endpoint config")
	}

	c.identityConfig, err = sdk.loadIdentityConfig(configBackend...)
	if err != nil {
		return nil, errors.WithMessage(err, "unable to load identity config")
	}

	c.metricsConfig, err = sdk.loadMetricsConfig(configBackend...)
	if err != nil {
		return nil, errors.WithMessage(err, "unable to load metrics config")
	}

	sdk.opts.ConfigBackend = configBackend

	return c, nil
}

func (sdk *IChainSDK) loadEndpointConfig(configBackend ...core.ConfigBackend) (ichain.EndpointConfig, error) {
	endpointConfigOpt, ok := sdk.opts.endpointConfig.(*endpoint.EndpointConfigOptions)

	if sdk.opts.endpointConfig == nil || (ok && !endpoint.IsEndpointConfigFullyOverridden(endpointConfigOpt)) {
		defEndpointConfig, err := endpoint.ConfigFromBackend(configBackend...)
		if err != nil {
			return nil, errors.WithMessage(err, "failed to initialize endpoint config from config backend")
		}

		if sdk.opts.endpointConfig == nil {
			return defEndpointConfig, nil
		}
		return endpoint.UpdateMissingOptsWithDefaultConfig(endpointConfigOpt, defEndpointConfig), nil
	}
	if !ok {
		return nil, errors.New("failed to retrieve config endpoint from opts")
	}
	return endpointConfigOpt, nil
}

func (sdk *IChainSDK) loadCryptoConfig(configBackend ...core.ConfigBackend) (core.CryptoSuiteConfig, error) {
	cryptoConfigOpt, ok := sdk.opts.CryptoSuiteConfig.(*cryptosuite.CryptoConfigOptions)

	if sdk.opts.CryptoSuiteConfig == nil || (ok && !cryptosuite.IsCryptoConfigFullyOverridden(cryptoConfigOpt)) {
		defCryptoConfig := cryptosuite.ConfigFromBackend(configBackend...)

		if sdk.opts.CryptoSuiteConfig == nil {
			return defCryptoConfig, nil
		}

		return cryptosuite.UpdateMissingOptsWithDefaultConfig(cryptoConfigOpt, defCryptoConfig), nil
	}

	if !ok {
		return nil, errors.New("failed to retrieve crypto suite configs from opts")
	}

	return cryptoConfigOpt, nil
}

func (sdk *IChainSDK) loadIdentityConfig(configBackend ...core.ConfigBackend) (iam.IdentityConfig, error) {
	identityConfigOpt, ok := sdk.opts.IdentityConfig.(*iam2.IdentityConfigOptions)

	if sdk.opts.IdentityConfig == nil || (ok && !iam2.IsIdentityConfigFullyOverridden(identityConfigOpt)) {
		defIdentityConfig, err := iam2.ConfigFromBackend(configBackend...)
		if err != nil {
			return nil, errors.WithMessage(err, "failed to initialize identity config from config backend")
		}

		if sdk.opts.IdentityConfig == nil {
			return defIdentityConfig, nil
		}

		return iam2.UpdateMissingOptsWithDefaultConfig(identityConfigOpt, defIdentityConfig), nil
	}

	if !ok {
		return nil, errors.New("failed to retrieve identity configs from opts")
	}

	return identityConfigOpt, nil
}

func (sdk *IChainSDK) loadMetricsConfig(configBackend ...core.ConfigBackend) (metricsCfg.MetricsConfig, error) {
	metricsConfigOpt, ok := sdk.opts.metricsConfig.(*metricsCfg.OperationsConfigOptions)

	if sdk.opts.metricsConfig == nil || (ok && !metricsCfg.IsMetricsConfigFullyOverridden(metricsConfigOpt)) {
		defMetricsConfig, err := metricsCfg.ConfigFromBackend(configBackend...)
		if err != nil {
			return nil, errors.WithMessage(err, "failed to initialize metrics config from config backend")
		}
		if sdk.opts.metricsConfig == nil {
			return defMetricsConfig, nil
		}

		return metricsCfg.UpdateMissingOptsWithDefaultConfig(metricsConfigOpt, defMetricsConfig), nil
	}

	if !ok {
		return nil, errors.New("failed to retrieve metrics configs from opts")
	}

	return metricsConfigOpt, nil
}

func withErrorHandlerProviderOpt(value ichain.ErrorHandler) coptions.Opt {
	return func(p coptions.Params) {
		if setter, ok := p.(errHandlerSetter); ok {
			logger.Debugf("... setting error handler")
			setter.SetErrorHandler(value)
		}
	}
}

type errHandlerSetter interface {
	SetErrorHandler(value ichain.ErrorHandler)
}
